
package com.rollway.restful;


import com.rollway.restful.options.CompositeRestfulOptions;
import com.rollway.restful.options.RestfulOptions;
import com.rollway.restful.support.ResponseAdapter;
import com.rollway.restful.utils.Util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import javafx.util.Pair;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.HttpMessageConverterExtractor;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Slf4j
public class RestfulMethodHandler implements MethodHandler {

    private MethodMetadata metadata;
    private CompositeRestfulOptions compositeRestFulOptions;
    private OkHttpClient okHttpClient;
    private List<HttpMessageConverter<?>> httpMessageConverters;
    private List<RequestCustomizer> requestCustomizers;

    public RestfulMethodHandler() {
    }

    public RestfulMethodHandler(MethodMetadata metadata, CompositeRestfulOptions compositeRestFulOptions, OkHttpClient okHttpClient, List<HttpMessageConverter<?>> httpMessageConverters, List<RequestCustomizer> requestCustomizers) {
        this.metadata = metadata;
        this.compositeRestFulOptions = compositeRestFulOptions;
        this.okHttpClient = okHttpClient;
        this.httpMessageConverters = httpMessageConverters;
        this.requestCustomizers = requestCustomizers;
    }

    @Override
    public Object invoke(Object[] argv) throws Throwable {
        Integer attemptsConfig = compositeRestFulOptions.getConfig(metadata, RestfulOptions.Options::getMaxAttempts).getValue();
        Integer attemptsTimeConfig = compositeRestFulOptions.getConfig(metadata, RestfulOptions.Options::getMaxAttemptTimeMillis).getValue();
        boolean retry = attemptsConfig != null || attemptsTimeConfig != null;
        Integer maxAttempts = 10;
        Integer maxAttemptTimeMillis = 60000;
        if (retry && attemptsConfig != null) {
            maxAttempts = Math.min(maxAttempts, attemptsConfig);
        }
        if (retry && attemptsTimeConfig != null) {
            maxAttemptTimeMillis = Math.min(maxAttemptTimeMillis, attemptsTimeConfig);
        }
        int attempt = 1;
        long start = System.currentTimeMillis();
        OkHttpClient requestScoped = getOkHttpClient();
        String url = buildUrl(argv);
        Request request = buildRequest(url, argv);
        while (true) {
            try {
                return execute(request, requestScoped);
            } catch (IOException e) {
                if (retry) {
                    long cost = System.currentTimeMillis() - start;
                    if (attempt++ >= maxAttempts || cost >= maxAttemptTimeMillis) {
                        log.error("{} already tried {} times and cost {} millisecond",request.url(),--attempt,cost);
                        throw e;
                    }
                    continue;
                }
                throw e;
            }
        }
    }

    private OkHttpClient getOkHttpClient() {
        Pair<Boolean, Integer> connectTimeoutPair = compositeRestFulOptions.getConfig(metadata, RestfulOptions.Options::getConnectTimeout);
        Pair<Boolean, Integer> readTimeoutPair = compositeRestFulOptions.getConfig(metadata, RestfulOptions.Options::getReadTimeout);
        OkHttpClient requestScoped;
        if (connectTimeoutPair.getKey() || readTimeoutPair.getKey()) {
            requestScoped = okHttpClient.newBuilder()
                    .connectTimeout(connectTimeoutPair.getValue(), TimeUnit.MILLISECONDS)
                    .readTimeout(readTimeoutPair.getValue(), TimeUnit.MILLISECONDS)
                    .build();
        } else {
            requestScoped = okHttpClient;
        }
        return requestScoped;
    }

    private String buildUrl(Object[] argv) {
        RestfulOptions restFulOptions = compositeRestFulOptions.getRestFulOptions(metadata);
        //build url
        StringBuilder urlBuilder = new StringBuilder();
        urlBuilder.append(restFulOptions.getScheme()).append(restFulOptions.getAuthority())
                .append(Util.valNoNullWithSep(restFulOptions.getPrefix()))
                .append(Util.valNoNullWithSep(restFulOptions.getPath()));
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(urlBuilder.toString());
        //build path
        Map<String, Object> uriVariables = new HashMap<>();
        metadata.getPathIndexAndName().forEach((key, value) -> {
            uriVariables.put(value, argv[key]);
        });
        uriComponentsBuilder.uriVariables(uriVariables);
        //build query map
        Integer queryMapIndex = metadata.getQueryMapIndex();
        if (queryMapIndex != null && argv[queryMapIndex] != null) {
            Map<String, Object> queryMap = (Map) argv[queryMapIndex];
            queryMap.forEach(uriComponentsBuilder::queryParam);
        }
        //build query
        metadata.getQueryIndexAndName().forEach((key, value) -> {
            uriComponentsBuilder.queryParam(value, argv[key]);
        });
        return uriComponentsBuilder.toUriString();
    }

    Object execute(Request request, OkHttpClient requestScoped) throws Throwable {
        Response response = requestScoped.newCall(request).execute();
        if (response.code() >= 200 && response.code() < 300) {
            Type returnType = metadata.getReturnType();
            if (void.class.equals(returnType)) {
                return null;
            }
            if (response.body() == null) {
                log.info("there is no response body from url:{}", request.url());
            }
            if (byte[].class.equals(returnType)) {
                return response.body().bytes();
            }
            if (returnType instanceof Class || returnType instanceof ParameterizedType || returnType instanceof WildcardType) {
                HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(returnType,
                        httpMessageConverters);
                return extractor.extractData(new ResponseAdapter(response));
            }
            throw new RuntimeException("type is not an instance of Class or ParameterizedType: " + returnType);
        } else {
            byte[] body = {};
            if (response.body() != null) {
                body = response.body().bytes();
            }
            String message = String.format("[%d] during [%s] to [%s] [%s]: [%s]"
                    , response.code(), request.method(), request.url(), metadata.getMethodKey(), new String(body, StandardCharsets.UTF_8));
            throw new RestfulException(response.code(), message);
        }
    }

    private Request buildRequest(String url, Object[] argv) throws IOException {
        Request.Builder requestBuilder = new Request.Builder();
        requestBuilder.url(url);
        metadata.getHeaders().forEach(requestBuilder::header);
        //build headerMap
        Integer headerMapIndex = metadata.getHeaderMapIndex();
        if (headerMapIndex != null && argv[headerMapIndex] != null) {
            Map<String, Object> headerMap = (Map<String, Object>) argv[headerMapIndex];
            headerMap.forEach((key, value) -> {
                appendHeader(requestBuilder, key, value);
            });
        }
        //build header
        metadata.getHeaderIndexAndName().forEach((key, value) -> {
            appendHeader(requestBuilder, value, argv[key]);
        });
        HttpMethod httpMethod = metadata.getHttpMethod();
        boolean isMethodWithBody =
                HttpMethod.POST == httpMethod || HttpMethod.PUT == httpMethod
                        || HttpMethod.PATCH == httpMethod;
        RequestBody requestBody = null;
        if (isMethodWithBody) {
            requestBody = extractRequestBody(argv, requestBuilder);
        }
        requestBuilder.method(metadata.getHttpMethod().name(), requestBody);
        return requestBuilder.build();
    }

    private RequestBody extractRequestBody(Object[] argv, Request.Builder requestBuilder) throws IOException {
        if(metadata.getBodyIndex() == null){
            return  RequestBody.create(null, new byte[0]);
        }
        Object body = argv[metadata.getBodyIndex()];
        //applyBuildCustomizers
        for (RequestCustomizer customizer : requestCustomizers) {
            body = customizer.customize(requestBuilder,body);
        }
        String contentType = requestBuilder.build().header(HttpHeaders.CONTENT_TYPE);
        MediaType mediaType = MediaType.parse(contentType);
        boolean multipartType = isMultipartType(contentType);
        if (multipartType) {
            if (body != null) {
                String bodyName = metadata.getBodyName();
                if (body instanceof MultipartFile) {
                    MultipartFile file = (MultipartFile) body;
                    return new MultipartBody.Builder()
                            .addFormDataPart(bodyName, file.getOriginalFilename(), RequestBody.create(MediaType.parse(contentType), file.getBytes()))
                            .build();
                } else if (body instanceof MultipartFile[]) {
                    MultipartFile[] files = (MultipartFile[]) body;
                    MultipartBody.Builder builder = new MultipartBody.Builder();
                    for (MultipartFile file : files) {
                        builder.addFormDataPart(bodyName, file.getOriginalFilename(), RequestBody.create(MediaType.parse(contentType), file.getBytes()));
                    }
                    return builder.build();
                } else if (body instanceof File) {
                    File file = (File) body;
                    return new MultipartBody.Builder()
                            .addFormDataPart(bodyName, file.getName(), RequestBody.create(MediaType.parse(contentType), file))
                            .build();
                } else if (body instanceof File[]) {
                    File[] files = (File[]) body;
                    MultipartBody.Builder builder = new MultipartBody.Builder();
                    for (File file : files) {
                        builder.addFormDataPart(bodyName, file.getName(), RequestBody.create(MediaType.parse(contentType), file));
                    }
                    return builder.build();
                } else {
                    throw new IllegalArgumentException("only supports file/MultipartFile and their array types;");
                }
            }
        }
        if (body == null) {
            body = new byte[0];
            log.warn("warning :{} got an empty body",metadata.getMethodKey());
        }
        byte[] bytes = toByteArray(body);
        return RequestBody.create(mediaType, bytes);
    }

    private void appendHeader(Request.Builder requestBuilder, String key, Object... value) {
        if (value == null || value.length == 0) {
            requestBuilder.header(key, Util.valNoNull(value));
        } else {
            for (Object val : value) {
                requestBuilder.header(key, Util.valNoNull(val));
            }
        }
    }

    private static byte[] toByteArray(Object obj) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.writeValueAsBytes(obj);
    }

    private boolean isMultipartType(String requestContentType) {
        return Arrays.asList(Util.MULTIPART_FORM_DATA, Util.MULTIPART_MIXED, Util.MULTIPART_RELATED)
                .contains(requestContentType);
    }
}
